import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.ARBVertexBufferObject;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.util.glu.GLU;


public class CubeDemo {
	private static final String TITLE = "Cube Demo";

	private static final int FRAMERATE = 60;

	private static final int WINDOW_WIDTH = 640;
	private static final int WINDOW_HEIGHT = 480;

	private volatile boolean isPaused = false;
	private volatile boolean isRunning = false;

    private float xrot = 0f;       // X Rotation
    private float yrot = 0f;       // Y Rotation
    private float zrot = 0f;       // Z Rotation
    private float xspeed = 0.3f;   // X Rotation Speed
    private float yspeed = 0.2f;   // Y Rotation Speed
    private float zspeed = 0.4f;   // Z Rotation Speed
    private float z = -5.0f;       // Depth Into The Screen

	/**
	 * Application init
	 *
	 * @param args
	 *            Commandline arguments
	 */
	public static void main(String[] args) {
		CubeDemo animation = null;
		try {
			animation = new CubeDemo();
			animation.run();
		} catch (Exception e) {
			e.printStackTrace(System.err);
			Sys.alert(TITLE, "An error occured and the game will exit.");
		} finally {
			if(animation != null) animation.cleanup();
		}

		System.exit(0);
	}

	/**
	 * Determine an available display that matches the specified paramaters.
	 *
	 * @param width
	 *            The desired width of the screen
	 * @param height
	 *            The desired height of the screen
	 * @param bpp
	 *            The desired colour depth (bits per pixel) of the screen
	 * @return The display mode matching the requirements or null if none could
	 *         be found
	 * @throws LWJGLException
	 *             Indicates a failure interacting with the LWJGL library.
	 */
	private static DisplayMode findDisplayMode(int width, int height, int bpp)
			throws LWJGLException {
		DisplayMode[] modes = Display.getAvailableDisplayModes();
		DisplayMode mode = null;

		for (int i = 0; i < modes.length; i++) {
			if ((modes[i].getBitsPerPixel() == bpp) || (mode == null)) {
				if ((modes[i].getWidth() == width)
						&& (modes[i].getHeight() == height)) {
					mode = modes[i];
				}
			}
		}

		return mode;
	}

	/**
	 * Initialise the animation
	 *
	 * @throws Exception
	 *             if init fails
	 */
	public CubeDemo() throws Exception {
		// find out what the current bits per pixel of the desktop is
		int currentBpp = Display.getDisplayMode().getBitsPerPixel();
		// find a display mode at 800x600
		DisplayMode mode = findDisplayMode(WINDOW_WIDTH, WINDOW_HEIGHT, currentBpp);

		// if can't find a mode, notify the user the give up
		if (mode == null) {
			Sys.alert("Error", WINDOW_WIDTH + "x" + WINDOW_HEIGHT + "x" + currentBpp
					+ " display mode unavailable");
			return;
		}

		Display.setTitle(TITLE);
		Display.setDisplayMode(mode);
		Display.setFullscreen(false);

		// Enable vsync if we can
		Display.setVSyncEnabled(true);

		Display.create();

		initGL();
	}

	/** The time since the last record of fps */
	private long lastFpsTime = 0;

	/** The recorded fps */
	private int fps = 0;

	/**
	 * Runs the animation (the "main loop")
	 */
	private void run() {
		long lastLoopTime, delta;

		Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

		isRunning = true;
		lastLoopTime = System.nanoTime();

		while (isRunning) {
			delta = System.nanoTime() - lastLoopTime;
			lastLoopTime = System.nanoTime();

			lastFpsTime += delta;
			fps++;

			// update our FPS counter if a second has passed
			if (lastFpsTime >= 1000000000L) {
				Display.setTitle(TITLE +" (FPS: " + fps + ")");
				lastFpsTime = 0;
				fps = 0;
			}

			// Always call Display.update(), all the time
			Display.update();

			if (Display.isCloseRequested()) {
				// Check for O/S close requests
				isRunning = false;
			} else if (Display.isActive()) {
				// The window is in the foreground, so we should play the game
				logic(delta);
				render();
				Display.sync(FRAMERATE);
			} else {
				// The window is not in the foreground, so we can allow other
				// stuff to run and
				// infrequently update
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
				}
				logic(delta);
				if (Display.isVisible() || Display.isDirty()) {
					// Only bother rendering if the window is visible or dirty
					render();
				}
			}
		}
	}

	private boolean pausePressed = false;
	private boolean resetPressed = false;

	/**
	 * Do all calculations, handle input, etc.
	 */
	private void logic(long delta) {
		if (Keyboard.isKeyDown(Keyboard.KEY_ESCAPE)) {
			isRunning = false;
		}
		if (Keyboard.isKeyDown(Keyboard.KEY_P) && !pausePressed) {
			pausePressed = true;
			isPaused = !isPaused;
		}
		if (!Keyboard.isKeyDown(Keyboard.KEY_P)) {
			pausePressed = false;
		}

		if (isPaused) return;

		if (Keyboard.isKeyDown(Keyboard.KEY_R) && !resetPressed) {
			resetPressed = true;
			// Reset speeds
			xspeed = 0.3f;
		    yspeed = 0.2f;
		    zspeed = 0.4f;
		}
		if (!Keyboard.isKeyDown(Keyboard.KEY_R)) {
			resetPressed = false;
		}

        if (Keyboard.isKeyDown(Keyboard.KEY_PRIOR)) {
            z -= 0.02f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_NEXT)) {
            z += 0.02f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
            xspeed -= 0.01f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
            xspeed += 0.01f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) {
            yspeed += 0.01f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) {
            yspeed -= 0.01f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_A)) {
        	zspeed -= 0.01f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_Z)) {
        	zspeed += 0.01f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_COMMA)) {
        	xspeed -= 0.01f;
        	yspeed -= 0.01f;
        	zspeed -= 0.01f;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_PERIOD)) {
        	xspeed += 0.01f;
        	yspeed += 0.01f;
        	zspeed += 0.01f;
        }

        xrot += xspeed; // X Axis Rotation
        yrot += yspeed; // Y Axis Rotation
        zrot += zspeed; // Z Axis Rotation
	}

    private void initGL() {
        GL11.glEnable(GL11.GL_TEXTURE_2D); // Enable Texture Mapping
        GL11.glShadeModel(GL11.GL_SMOOTH); // Enable Smooth Shading
        GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Black Background
        GL11.glClearDepth(1.0); // Depth Buffer Setup
        GL11.glEnable(GL11.GL_DEPTH_TEST); // Enables Depth Testing
        GL11.glDepthFunc(GL11.GL_LEQUAL); // The Type Of Depth Testing To Do

        GL11.glMatrixMode(GL11.GL_PROJECTION); // Select The Projection Matrix
        GL11.glLoadIdentity(); // Reset The Projection Matrix

        // Calculate The Aspect Ratio Of The Window
        GLU.gluPerspective(
          45.0f,
          (float) WINDOW_WIDTH / (float) WINDOW_HEIGHT,
          0.1f,
          100.0f);
        GL11.glMatrixMode(GL11.GL_MODELVIEW); // Select The Modelview Matrix

        // Really Nice Perspective Calculations
        GL11.glHint(GL11.GL_PERSPECTIVE_CORRECTION_HINT, GL11.GL_NICEST);
     
        
        vertices = BufferUtils.createFloatBuffer(3 * 4 * 6);
        vertices.put(new float[] {
        		-1.0f, -1.0f, 1.0f,
        		1.0f, -1.0f, 1.0f,
        		1.0f, 1.0f, 1.0f,
        		-1.0f, 1.0f, 1.0f,
        		
        		-1.0f, -1.0f, -1.0f,
        		-1.0f, 1.0f, -1.0f,
        		1.0f, 1.0f, -1.0f,
        		1.0f, -1.0f, -1.0f,
        		
        		-1.0f, 1.0f, -1.0f,
        		-1.0f, 1.0f, 1.0f,
        		1.0f, 1.0f, 1.0f,
        		1.0f, 1.0f, -1.0f,
        		
        		-1.0f, -1.0f, -1.0f,
        		1.0f, -1.0f, -1.0f,
        		1.0f, -1.0f, 1.0f,
        		-1.0f, -1.0f, 1.0f,
        		
        		1.0f, -1.0f, -1.0f,
        		1.0f, 1.0f, -1.0f,
        		1.0f, 1.0f, 1.0f,
        		1.0f, -1.0f, 1.0f,
        		
        		-1.0f, -1.0f, -1.0f,
        		-1.0f, -1.0f, 1.0f,
        		-1.0f, 1.0f, 1.0f,
        		-1.0f, 1.0f, -1.0f});
        vertices.rewind();
        
        vertexBufferID = createVBOID();
        bufferData(vertexBufferID, vertices);
    }
    
    private int vertexBufferID;
    private FloatBuffer vertices;
    
    public static int createVBOID() {
		if (GLContext.getCapabilities().GL_ARB_vertex_buffer_object) {
			IntBuffer buffer = BufferUtils.createIntBuffer(1);
			ARBVertexBufferObject.glGenBuffersARB(buffer);
			return buffer.get(0);
		}
		return 0;
	}
    
    public static void bufferData(int id, FloatBuffer buffer) {
		ARBVertexBufferObject.glBindBufferARB(ARBVertexBufferObject.GL_ARRAY_BUFFER_ARB, id);
		ARBVertexBufferObject.glBufferDataARB(ARBVertexBufferObject.GL_ARRAY_BUFFER_ARB, buffer, ARBVertexBufferObject.GL_STATIC_DRAW_ARB);
	}   

	/**
	 * Render the current frame
	 */
	private void render() {
        GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer

        GL11.glLoadIdentity(); // Reset The Current Modelview Matrix

        GL11.glTranslatef(0.0f, 0.0f, z); // Move Into The Screen 5 Units
        GL11.glRotatef(xrot, 1.0f, 0.0f, 0.0f); // Rotate On The X Axis
        GL11.glRotatef(yrot, 0.0f, 1.0f, 0.0f); // Rotate On The Y Axis
        GL11.glRotatef(zrot, 0.0f, 0.0f, 1.0f); // Rotate On The Z Axis
        GL11.glColor3f(1.0f, 1.0f, 1.0f);
        
        vertices.rewind();
        
        GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY);
        ARBVertexBufferObject.glBindBufferARB(ARBVertexBufferObject.GL_ARRAY_BUFFER_ARB, vertexBufferID);
        GL11.glVertexPointer(3, GL11.GL_FLOAT, 0, 0);
        GL11.glDrawArrays(GL11.GL_QUADS, 0, 24);
        GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY);
	}

	/**
	 * Do any animation-specific cleanup
	 */
	private void cleanup() {
		// Close the window
		Display.destroy();
	}
}